Skip to content

feat: add Contributor Activity Matrix with GitHub API integration#33

Open
Muneerali199 wants to merge 1 commit intoAOSSIE-Org:mainfrom
Muneerali199:main
Open

feat: add Contributor Activity Matrix with GitHub API integration#33
Muneerali199 wants to merge 1 commit intoAOSSIE-Org:mainfrom
Muneerali199:main

Conversation

@Muneerali199
Copy link
Contributor

@Muneerali199 Muneerali199 commented Mar 2, 2026

fix #30
Added Contributor Activity Matrix feature that tracks PRs, issues, and repositories across GitHub organizations with caching, rate limit handling, and optional token for higher limits.

demo video share in discord .

Summary by CodeRabbit

Release Notes

  • New Features

    • Added multi-page navigation with home and organization-specific contributor views
    • Added GitHub token management with secure localStorage persistence
    • Added contributor activity matrix with per-repository contribution tracking
    • Added contributor statistics dashboard with top contributors and repositories
    • Added filtering, search, and sorting capabilities for contributors
    • Added data export functionality in CSV and JSON formats
    • Added cache management and data refresh controls
  • Chores

    • Added environment configuration support for GitHub token setup
    • Updated to light theme styling across the application
    • Added routing library dependency

@coderabbitai
Copy link

coderabbitai bot commented Mar 2, 2026

Walkthrough

This pull request introduces a "Contributor Activity Matrix" dashboard feature for the OrgExplorer application. It adds React Router-based multi-page navigation, a comprehensive contributor activity view with filtering and export capabilities, GitHub API integration with caching and rate limiting, and associated UI components for displaying contributor statistics and activity matrices.

Changes

Cohort / File(s) Summary
Application Routing & Layout
src/App.tsx, src/App.css, src/index.css
Refactored App to use React Router with Home and per-organization contributor pages. Added extensive CSS styling (~450 lines) for navbar, layout, components, cards, matrix, loading/error states, and light theme defaults. Updated base styles to light color scheme.
Feature Components
src/features/contributor-activity/ContributorActivityFeature.tsx, src/features/contributor-activity/components/ContributorMatrix.tsx, src/features/contributor-activity/components/ContributorStats.tsx, src/features/contributor-activity/components/ContributorFilters.tsx
Introduced new React components for the contributor activity dashboard: main feature component managing state and orchestrating child components; matrix component rendering activity grid with repos and contributors; stats component displaying aggregated metrics; filters component for search, filtering, sorting, and data export.
UI State Components
src/features/contributor-activity/components/LoadingState.tsx, src/features/contributor-activity/components/ErrorState.tsx
Added simple UI components for loading and error states with optional retry callback.
Token Management
src/features/contributor-activity/components/GitHubTokenInput.tsx
Introduced component for GitHub token input with localStorage persistence, visibility toggle, and save/clear actions.
Feature Exports
src/features/contributor-activity/index.ts
Created barrel export file exposing main feature and all sub-components.
Business Logic & Use Cases
src/features/contributor-activity/useCases.ts
Added six use-case classes for fetching, filtering, searching, aggregating stats, and exporting contributor data.
GitHub API Client
src/lib/github/client.ts
Implemented comprehensive GitHub REST API client with in-memory caching, rate-limit handling, retry logic, and paginated search methods for repositories, pull requests, and issues. Includes header construction with optional token injection from localStorage.
GitHub API Aggregation
src/lib/github/api.ts
Built high-level API layer aggregating PRs, issues, and repositories into contributor activity matrices with stats calculation and rate-limit checking.
Type Definitions
src/lib/github/types.ts
Defined TypeScript interfaces for GitHub entities (Repository, Author, PullRequest, Issue) and domain models (ContributorSummary, ContributorMatrixData, RateLimitInfo).
Configuration
.env.example, package.json
Added environment variable template for GitHub token and react-router-dom dependency.

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant App as App (React Router)
    participant Feature as ContributorActivityFeature
    participant GitHubTokenInput
    participant LocalStorage
    participant Client as GitHub API Client
    participant API as GitHub API
    participant UseCases as Use Cases
    participant Matrix as ContributorMatrix

    User->>App: Navigate to /contributors/:org
    App->>Feature: Render with org param & fromDate
    Feature->>GitHubTokenInput: Initialize token input
    GitHubTokenInput->>LocalStorage: Read stored token
    LocalStorage-->>GitHubTokenInput: Return token (if exists)
    GitHubTokenInput-->>Feature: Invoke onTokenChange callback
    
    Feature->>Feature: Set loading state
    Feature->>UseCases: FetchContributorActivityUseCase.execute(org, fromDate)
    UseCases->>Client: fetchAllOrgRepositories(org)
    Client->>API: GET /repos?org=X
    API-->>Client: Return repos with pagination
    Client-->>UseCases: Return full repo list
    
    par Parallel Requests
        UseCases->>Client: searchAllPRs(org, fromDate)
        UseCases->>Client: searchAllIssues(org, fromDate)
    end
    
    Client->>API: GET /search?q=org:X is:pr created:>date
    Client->>API: GET /search?q=org:X is:issue created:>date
    API-->>Client: Return PRs and Issues
    Client-->>UseCases: Return aggregated data
    
    UseCases->>UseCases: transformToContributorMatrix()
    UseCases-->>Feature: Return ContributorMatrixData
    
    Feature->>UseCases: GetContributorStatsUseCase.execute(data)
    UseCases-->>Feature: Return stats
    
    Feature->>Feature: Apply filters & search
    Feature->>Feature: Set data state
    
    Feature->>Matrix: Render with data & repos
    Matrix-->>User: Display activity matrix
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Suggested labels

Typescript Lang

Poem

🐰 A matrix of hops, through repos we bound,
Contributors dancing, their pull-requests found,
With GraphQL searching and caches so fleet,
The GSoC dashboard is now complete!

🚥 Pre-merge checks | ✅ 3 | ❌ 1

❌ Failed checks (1 inconclusive)

Check name Status Explanation Resolution
Linked Issues check ❓ Inconclusive The PR partially addresses issue #30 objectives. It implements a Contributor Activity Matrix with GitHub API integration, filtering, search, stats, and export capabilities. However, it uses REST API with pagination instead of the requested GraphQL search query for optimal performance. Verify if the REST API implementation with caching and rate-limit handling meets performance requirements, or consider migrating to GraphQL search query as originally proposed in issue #30 for maximum efficiency.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures the main feature being added: a Contributor Activity Matrix with GitHub API integration.
Out of Scope Changes check ✅ Passed All changes are directly related to implementing the Contributor Activity Matrix feature. Global styling updates, routing infrastructure, and API client additions all support the core feature objectives.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@github-actions github-actions bot added size/XL and removed size/XL labels Mar 2, 2026
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 24

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/App.tsx`:
- Around line 11-18: The JSX includes hardcoded user-visible strings (e.g., the
<h1> "OrgExplorer" and Link labels inside the div.nav-links) which must be
externalized; update the App component to replace these literal strings with
i18n lookup keys (e.g., use a translation function like t('nav.title') and
t('nav.home'), t('nav.contributors.aossie') etc.), import and use the project's
i18n helper (or react-i18next useTranslation) at the top of the component, and
add corresponding entries to the resource file(s); ensure the same refactor is
applied for the other hardcoded strings referenced (lines ~38-53) so all
user-visible text comes from the localization resources.
- Around line 60-64: Validate and sanitize the URL-derived values before passing
them into ContributorActivityFeature: parse and decode the org from
window.location.pathname using decodeURIComponent and a safer extraction (e.g.,
take the last non-empty segment), trim and validate it against a whitelist regex
(alphanumeric, hyphen, underscore) and fallback to 'AOSSIE-Org' if it fails; for
fromDate (params.get('from')), validate it as an ISO date (YYYY-MM-DD) or use
Date.parse to ensure it's a real date, normalize it to the expected format (or
set to undefined) and only pass the validated values into
ContributorActivityFeature (references: params, org, fromDate,
ContributorActivityFeature).

In `@src/features/contributor-activity/components/ContributorFilters.tsx`:
- Around line 27-32: The search input in ContributorFilters (the input using
value={searchQuery} and onChange={e => onSearchChange(e.target.value)}) needs an
accessible name—add a visible <label> or an aria-label/aria-labelledby that
describes it (e.g., "Search contributors") so screen readers can identify it;
also locate the buttons in the same component (the ones around the 74-80 area)
and ensure each has an explicit type attribute (e.g., type="button" or
type="submit" as appropriate) to prevent unintended form submissions.

In `@src/features/contributor-activity/components/ContributorMatrix.tsx`:
- Around line 31-48: The JSX in ContributorMatrix contains hardcoded
user-visible strings (“Contributor”, “Total”, the no-activity message, and any
"Less"/"More" labels) which must be externalized for i18n; update the
ContributorMatrix component to import and use the project’s localization utility
(e.g., a t() or useTranslation hook) and replace the literal strings in the
render (including the table header labels, the no-results paragraph, and any
pagination/buttons that render "Less"/"More") with localized keys (e.g.,
t('contributorMatrix.contributor'), t('contributorMatrix.total'),
t('contributorMatrix.noActivity'), etc.), add those keys to the resource files,
and ensure title/tooltips like repo header titles also use the localized strings
where appropriate so all user-facing text comes from the resource bundle instead
of hardcoded literals.
- Around line 12-13: Replace the hard-coded "15" repository limit with a named
constant to avoid duplication: define a constant (e.g. const REPO_DISPLAY_LIMIT
= 15) near the top of ContributorMatrix.tsx and replace uses of the literal in
the expressions that create displayRepos (allRepositories.slice(0, 15)) and
hasMoreRepos (allRepositories.length > 15) as well as any other occurrences (the
logic around line 81) to reference REPO_DISPLAY_LIMIT so the limit is maintained
in one place.
- Around line 15-18: getActivityCount currently does a linear search through
data.contributors for each cell; switch it to use the O(1) lookup provided by
data.activityByContributor (from ContributorMatrixData). In
ContributorMatrix.tsx replace the find-based logic in getActivityCount with a
lookup like: get the contributorMap = data.activityByContributor.get(login) and
then return contributorMap?.get(repo) || 0, ensuring you handle missing map or
missing repo by returning 0. Also keep the function signature and callers
unchanged so rendering logic still receives a number.
- Around line 60-66: The anchor HREF should not branch on
contributor.login.includes('http'); update the ContributorMatrix component to
always generate profile links using `https://github.com/${contributor.login}`
and remove the defensive includes('http') check; instead validate/normalize
logins when data is fetched or transformed (e.g., in your contributor data
mapper or a normalizeContributors / mapContributors function) to strip/handle
any URLs or malformed values and ensure contributor.login is a plain GitHub
username before it reaches ContributorMatrix.

In `@src/features/contributor-activity/components/ContributorStats.tsx`:
- Around line 21-26: The component ContributorStats currently hardcodes
user-visible strings (e.g., "Total Contributors", "Avg. Contributions", "Top
Contributors", "Most Active Repos") inside the JSX; replace those literals with
references to i18n resource keys and use the existing localization helper (e.g.,
call the i18n/t function or useTranslation hook used in the project) inside
ContributorStats so the headings and any labels render via resource strings
(e.g., t('contributorStats.totalContributors'),
t('contributorStats.avgContributions'), etc.), add the corresponding keys and
translations to the locale resource file(s), and ensure stats.totalContributors
and stats.averageContributions remain unchanged while labels come from the i18n
keys.

In `@src/features/contributor-activity/components/ErrorState.tsx`:
- Around line 6-17: The ErrorState component renders a hardcoded "Retry" label;
replace this with an externalized i18n string: add a resource key (e.g.,
"retry") to your locale files and consume it in ErrorState (use your app's
localization API such as useTranslation/useIntl or accept a translated prop)
instead of the literal "Retry" in the button; update the locale resource files
to include the new key and ensure a sensible fallback is used if the translation
is missing.
- Around line 11-13: The Retry button in the ErrorState component defaults to
type="submit" which can trigger form submissions; update the button element that
uses onRetry and className "btn-retry" to include an explicit type="button"
attribute so clicking it only invokes the onRetry handler and never submits a
surrounding form.

In `@src/features/contributor-activity/components/GitHubTokenInput.tsx`:
- Around line 39-46: The label in the GitHubTokenInput component is not tied to
the input and the toggle/clear action buttons lack explicit button semantics;
add a unique id for the input (e.g., tokenInputId) and set the label's htmlFor
to that id so the label is associated with the input (affecting the input
controlled by value={token}, onChange={setToken} and type driven by showToken),
and update the action elements (the show/hide toggle and any clear/save buttons
referenced around the token input, lines with showToken and token state
handling) to use <button type="button"> semantics rather than implicit buttons
or divs to ensure correct accessibility and prevent accidental form submissions.
- Around line 14-19: The useEffect reads TOKEN_KEY from localStorage and calls
onTokenChange(savedToken) but does not initialize the component's local input
state, leaving the input empty and hiding the Clear action; update the effect to
set the component's token state as well (e.g., call setToken(savedToken) or
initialize token state with localStorage.getItem(TOKEN_KEY)) so the input
displays the persisted value and the Clear action is visible; ensure this change
touches the same effect using TOKEN_KEY and uses the existing state setter
(setToken) alongside onTokenChange to avoid redundant reloads.
- Around line 15-33: The component currently persists the PAT using localStorage
(TOKEN_KEY) in useEffect, handleSave and handleClear which exposes sensitive
tokens; remove all localStorage.getItem/setItem/removeItem calls and keep the
token only in React state (token) and via onTokenChange, or alternatively send
the token to a secure backend endpoint (or use sessionStorage if ephemeral
storage is required) instead of localStorage; update handleSave to avoid writing
to localStorage (setSaved behavior can remain but should be driven by
state/response from backend if used), and update handleClear to only clear state
and notify onTokenChange('') without removing localStorage. Ensure TOKEN_KEY
usage is eliminated and onTokenChange remains the mechanism to propagate the
token to the rest of the app or backend.

In `@src/features/contributor-activity/components/LoadingState.tsx`:
- Around line 1-8: The LoadingState component currently hardcodes the message
and lacks ARIA attributes; update the LoadingState React.FC to retrieve the
display string from the i18n/messages system (replace the literal "Loading
contributor activity..." with the localized message key) and add proper
accessibility attributes to the container and spinner (e.g., role="status" or
aria-live="polite" on the text container and aria-hidden/aria-busy as
appropriate on the spinner) so screen readers announce the state; update
references in the component (LoadingState, the spinner div and the paragraph) to
use the localized string and ARIA attributes.

In `@src/features/contributor-activity/ContributorActivityFeature.tsx`:
- Around line 171-173: The Clear Cache button currently lacks an explicit type
which can cause it to act as a submit button in forms; update the JSX for the
button element (the one using onClick={handleClearCache} and
className="btn-clear-cache") to include type="button" so it won't trigger form
submission inadvertently and continues to call the handleClearCache handler.

In `@src/features/contributor-activity/useCases.ts`:
- Around line 81-84: The JSON branch of execute in ContributorMatrixData
currently calls JSON.stringify(data) which loses Map contents because
contributors[].repositories and activityByContributor are Maps; convert those
Maps to plain objects/arrays before stringifying: iterate contributors (in the
execute function) and replace each contributor.repositories Map with an object
(or array of [key,value] pairs), and similarly transform activityByContributor
(Map<string, Map<string, number>>) into a nested plain object or serializable
structure, then call JSON.stringify on the transformed object so all map data is
preserved.

In `@src/lib/github/api.ts`:
- Around line 55-57: Guard against empty repository names before inserting into
collections: when computing repoName (from pr.repository_url?.split('/').pop()),
check that repoName is a non-empty string before calling repoSet.add(repoName)
and before updating contributor repositories and the activity matrix (the
analogous updates around lines 115-117). Wrap those add/update calls in a
conditional like if (repoName) { ... } so blank strings are never used as keys.
- Around line 10-13: searchAllIssues currently filters only open issues which
undercounts contributor activity; update the searchAllIssues query to remove the
"is:open" qualifier (or explicitly include both open and closed) so all issues
are returned, then re-run the existing aggregation that consumes
searchAllIssues; also fix repoName handling where repository_url may be missing
by avoiding defaulting to an empty string—change the logic that derives repoName
(the variable computed from repository_url in api functions that aggregate
contributors after calling searchAllPRs/searchAllIssues/fetchAllOrgRepositories)
to either skip entries with no repository_url or use a non-empty sentinel (e.g.,
"unknown" or null) and ensure contributor maps keyed by repoName do not get
blank keys (filter out or normalize empty repoName before inserting into
contributor maps).

In `@src/lib/github/client.ts`:
- Around line 63-66: The code reads a persisted GitHub token from localStorage
(the token variable) and sets headers['Authorization'], which exposes
credentials; remove the localStorage.getItem('github_token') usage and stop
persisting PATs client-side. Change the consumer of this module to pass an
ephemeral token into the function that builds headers (or wire the function to
fetch auth from a secure server-side proxy), and update the code path that sets
headers['Authorization'] (the assignment to headers['Authorization'] = `token
${token}`) to only run when a token is provided via the new parameter or secure
backend call; also remove any code that writes tokens into localStorage.
- Line 285: The searchAllIssues implementation currently forces only open issues
by building the query string with "is:open"; update the query construction in
searchAllIssues to remove or make the "is:open" filter conditional so it can
include closed issues when callers request "all" (or default to no state filter
for an all-issues endpoint). Locate the query string assembly where `const query
= \`org:${org} is:issue is:open${sinceDate ? \` created:>${sinceDate}\` :
''}\`;` is defined and adjust it to either omit `is:open` or accept a parameter
(e.g., issueState) to append `is:open` or `is:closed` only when needed, ensuring
callers can request all issues. Ensure tests or call sites of searchAllIssues
are updated to pass the desired state when appropriate.
- Around line 133-140: The current loop calls getRateLimit() before each request
which doubles API calls; instead remove the preflight getRateLimit() invocation
in the retry loop and extract rate-limit info from the actual request response
headers (e.g., read response.headers.get('x-ratelimit-remaining') and
'x-ratelimit-reset') to decide backoff and delay; update the retry logic inside
the for-loop that uses maxRetries/attempt and delay() so it inspects those
headers after a response (or on error when available) and computes waitTime =
(reset - Date.now()/1000)*1000, logging and awaiting delay(waitTime) when
remaining < 1 and waitTime > 0.
- Around line 3-7: Remove the duplicate RateLimitInfo interface from
src/lib/github/client.ts and instead import the existing type from the shared
declaration (src/lib/github/types.ts); specifically delete the local
RateLimitInfo declaration and add an import for RateLimitInfo at the top of
client.ts, then ensure any references in functions like the client methods still
use the imported RateLimitInfo type.

In `@src/lib/github/types.ts`:
- Around line 56-60: The RateLimitInfo interface is duplicated; consolidate to a
single source by keeping one declaration and importing it where needed: either
export RateLimitInfo from the types module and remove the duplicate in the
client module (then import RateLimitInfo in client.ts), or delete RateLimitInfo
from types.ts and import the single definition from client.ts; update any
imports/usages to reference the retained symbol RateLimitInfo and remove the
redundant interface declaration.
- Around line 34-42: The JSON export currently serializes ContributorMatrixData
containing Map fields (repositories and activityByContributor) which become
empty objects; in ExportContributorDataUseCase.execute convert any Map<string,
number> fields (e.g., ContributorSummary.repositories and top-level
activityByContributor in ContributorMatrixData) into plain objects before
calling JSON.stringify — for example, replace Maps with Object.fromEntries(map)
or build a plain object via Array.from(map.entries()) for each map, or supply a
JSON.stringify replacer that transforms Map instances to plain objects; ensure
you perform this transformation for ContributorSummary objects inside the matrix
as well as the top-level maps so the exported JSON contains the actual key/value
pairs.

ℹ️ Review info

Configuration used: Path: .coderabbit.yaml

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 1cfc3e7 and f2c2a75.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (17)
  • .env.example
  • package.json
  • src/App.css
  • src/App.tsx
  • src/features/contributor-activity/ContributorActivityFeature.tsx
  • src/features/contributor-activity/components/ContributorFilters.tsx
  • src/features/contributor-activity/components/ContributorMatrix.tsx
  • src/features/contributor-activity/components/ContributorStats.tsx
  • src/features/contributor-activity/components/ErrorState.tsx
  • src/features/contributor-activity/components/GitHubTokenInput.tsx
  • src/features/contributor-activity/components/LoadingState.tsx
  • src/features/contributor-activity/index.ts
  • src/features/contributor-activity/useCases.ts
  • src/index.css
  • src/lib/github/api.ts
  • src/lib/github/client.ts
  • src/lib/github/types.ts

Comment on lines +11 to +18
<h1>OrgExplorer</h1>
</div>
<div className="nav-links">
<Link to="/">Home</Link>
<Link to="/contributors/AOSSIE-Org">AOSSIE Contributors</Link>
<Link to="/contributors/StabilityNexus">StabilityNexus Contributors</Link>
<Link to="/contributors/DjedAlliance">DjedAlliance Contributors</Link>
</div>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Externalize user-visible strings instead of hardcoding them in components.

The new navigation/homepage text is directly embedded in JSX, which blocks localization scalability.

As per coding guidelines, "User-visible strings should be externalized to resource files (i18n)."

Also applies to: 38-53

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 11 - 18, The JSX includes hardcoded user-visible
strings (e.g., the <h1> "OrgExplorer" and Link labels inside the div.nav-links)
which must be externalized; update the App component to replace these literal
strings with i18n lookup keys (e.g., use a translation function like
t('nav.title') and t('nav.home'), t('nav.contributors.aossie') etc.), import and
use the project's i18n helper (or react-i18next useTranslation) at the top of
the component, and add corresponding entries to the resource file(s); ensure the
same refactor is applied for the other hardcoded strings referenced (lines
~38-53) so all user-visible text comes from the localization resources.

Comment on lines +60 to +64
const params = new URLSearchParams(window.location.search);
const org = window.location.pathname.split('/').pop() || 'AOSSIE-Org';
const fromDate = params.get('from') || undefined;

return <ContributorActivityFeature organization={org} defaultFromDate={fromDate} />;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Validate URL-derived org and from before passing them downstream.

from is forwarded without format validation, and org parsing is brittle. This can produce malformed GitHub search qualifiers and inconsistent results.

🔧 Proposed fix
 const ContributorActivityFeatureWrapper: React.FC = () => {
   const params = new URLSearchParams(window.location.search);
-  const org = window.location.pathname.split('/').pop() || 'AOSSIE-Org';
-  const fromDate = params.get('from') || undefined;
+  const orgCandidate = window.location.pathname.split('/').filter(Boolean).pop() ?? 'AOSSIE-Org';
+  const org = /^[A-Za-z0-9-]+$/.test(orgCandidate) ? orgCandidate : 'AOSSIE-Org';
+  const fromParam = params.get('from');
+  const fromDate = fromParam && /^\d{4}-\d{2}-\d{2}$/.test(fromParam) ? fromParam : undefined;
   
   return <ContributorActivityFeature organization={org} defaultFromDate={fromDate} />;
 };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/App.tsx` around lines 60 - 64, Validate and sanitize the URL-derived
values before passing them into ContributorActivityFeature: parse and decode the
org from window.location.pathname using decodeURIComponent and a safer
extraction (e.g., take the last non-empty segment), trim and validate it against
a whitelist regex (alphanumeric, hyphen, underscore) and fallback to
'AOSSIE-Org' if it fails; for fromDate (params.get('from')), validate it as an
ISO date (YYYY-MM-DD) or use Date.parse to ensure it's a real date, normalize it
to the expected format (or set to undefined) and only pass the validated values
into ContributorActivityFeature (references: params, org, fromDate,
ContributorActivityFeature).

Comment on lines +27 to +32
<input
type="text"
placeholder="Search contributors..."
value={searchQuery}
onChange={(e) => onSearchChange(e.target.value)}
/>
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Add accessible naming for search input and explicit button types.

🔧 Proposed fix
         <input
           type="text"
+          aria-label="Search contributors"
           placeholder="Search contributors..."
           value={searchQuery}
           onChange={(e) => onSearchChange(e.target.value)}
         />
@@
-        <button onClick={onRefresh} className="btn-refresh">
+        <button type="button" onClick={onRefresh} className="btn-refresh">
           Refresh
         </button>
-        <button onClick={() => onExport('csv')} className="btn-export">
+        <button type="button" onClick={() => onExport('csv')} className="btn-export">
           Export CSV
         </button>
-        <button onClick={() => onExport('json')} className="btn-export">
+        <button type="button" onClick={() => onExport('json')} className="btn-export">
           Export JSON
         </button>

Also applies to: 74-80

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/contributor-activity/components/ContributorFilters.tsx` around
lines 27 - 32, The search input in ContributorFilters (the input using
value={searchQuery} and onChange={e => onSearchChange(e.target.value)}) needs an
accessible name—add a visible <label> or an aria-label/aria-labelledby that
describes it (e.g., "Search contributors") so screen readers can identify it;
also locate the buttons in the same component (the ones around the 74-80 area)
and ensure each has an explicit type attribute (e.g., type="button" or
type="submit" as appropriate) to prevent unintended form submissions.

Comment on lines +12 to +13
const displayRepos = allRepositories.slice(0, 15);
const hasMoreRepos = allRepositories.length > 15;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick | 🔵 Trivial

Extract magic number to a named constant.

The limit of 15 repositories appears twice (lines 12, 81). Extract to a constant for maintainability.

♻️ Suggested refactor
+const MAX_DISPLAY_REPOS = 15;
+
 export const ContributorMatrix: React.FC<ContributorMatrixProps> = ({
   data,
   allRepositories,
 }) => {
-  const displayRepos = allRepositories.slice(0, 15);
-  const hasMoreRepos = allRepositories.length > 15;
+  const displayRepos = allRepositories.slice(0, MAX_DISPLAY_REPOS);
+  const hasMoreRepos = allRepositories.length > MAX_DISPLAY_REPOS;

Also update line 81:

-              {hasMoreRepos && <td className="more-cell">+{allRepositories.length - 15}</td>}
+              {hasMoreRepos && <td className="more-cell">+{allRepositories.length - MAX_DISPLAY_REPOS}</td>}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/contributor-activity/components/ContributorMatrix.tsx` around
lines 12 - 13, Replace the hard-coded "15" repository limit with a named
constant to avoid duplication: define a constant (e.g. const REPO_DISPLAY_LIMIT
= 15) near the top of ContributorMatrix.tsx and replace uses of the literal in
the expressions that create displayRepos (allRepositories.slice(0, 15)) and
hasMoreRepos (allRepositories.length > 15) as well as any other occurrences (the
logic around line 81) to reference REPO_DISPLAY_LIMIT so the limit is maintained
in one place.

Comment on lines +15 to +18
const getActivityCount = (login: string, repo: string): number => {
const contributor = data.contributors.find(c => c.login === login);
return contributor?.repositories.get(repo) || 0;
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Inefficient O(n) lookup for each cell; use activityByContributor Map instead.

getActivityCount performs a linear search through data.contributors for every cell rendered. With C contributors and R repositories, this results in O(C × R × C) complexity.

The data.activityByContributor Map (defined in ContributorMatrixData) appears designed for O(1) lookups but isn't being used.

🔧 Proposed fix using the existing Map
   const getActivityCount = (login: string, repo: string): number => {
-    const contributor = data.contributors.find(c => c.login === login);
-    return contributor?.repositories.get(repo) || 0;
+    return data.activityByContributor.get(login)?.get(repo) || 0;
   };
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/features/contributor-activity/components/ContributorMatrix.tsx` around
lines 15 - 18, getActivityCount currently does a linear search through
data.contributors for each cell; switch it to use the O(1) lookup provided by
data.activityByContributor (from ContributorMatrixData). In
ContributorMatrix.tsx replace the find-based logic in getActivityCount with a
lookup like: get the contributorMap = data.activityByContributor.get(login) and
then return contributorMap?.get(repo) || 0, ensuring you handle missing map or
missing repo by returning 0. Also keep the function signature and callers
unchanged so rendering logic still receives a number.

Comment on lines +63 to +66
const token = localStorage.getItem('github_token');
if (token) {
headers['Authorization'] = `token ${token}`;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Do not persist GitHub tokens in localStorage.

Storing PATs in localStorage increases exposure to XSS/token theft. Move token handling to a server-side proxy (preferred) or at minimum avoid persistent client-side storage for sensitive credentials.

As per coding guidelines, Security requires "No exposed API keys or sensitive data".

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/github/client.ts` around lines 63 - 66, The code reads a persisted
GitHub token from localStorage (the token variable) and sets
headers['Authorization'], which exposes credentials; remove the
localStorage.getItem('github_token') usage and stop persisting PATs client-side.
Change the consumer of this module to pass an ephemeral token into the function
that builds headers (or wire the function to fetch auth from a secure
server-side proxy), and update the code path that sets headers['Authorization']
(the assignment to headers['Authorization'] = `token ${token}`) to only run when
a token is provided via the new parameter or secure backend call; also remove
any code that writes tokens into localStorage.

Comment on lines +133 to +140
for (let attempt = 0; attempt < maxRetries; attempt++) {
const rateLimit = await getRateLimit();

if (rateLimit.remaining < 1) {
const waitTime = (rateLimit.reset - Date.now() / 1000) * 1000;
if (waitTime > 0 && waitTime < 60000) {
console.log(`Rate limit hit. Waiting ${Math.round(waitTime / 1000)}s...`);
await delay(waitTime);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# First, let's look at the structure and the specific file
find . -name "client.ts" -path "*/github/*" | head -20

Repository: AOSSIE-Org/OrgExplorer

Length of output: 92


🏁 Script executed:

# Read the file to understand the context around lines 133-140
wc -l src/lib/github/client.ts

Repository: AOSSIE-Org/OrgExplorer

Length of output: 94


🏁 Script executed:

# Get the relevant section
sed -n '120,160p' src/lib/github/client.ts

Repository: AOSSIE-Org/OrgExplorer

Length of output: 1498


🏁 Script executed:

# Find the getRateLimit function definition
rg -n "getRateLimit" src/lib/github/client.ts -A 10

Repository: AOSSIE-Org/OrgExplorer

Length of output: 831


🏁 Script executed:

# Search for getRateLimit definition anywhere in the codebase
rg -n "function getRateLimit|const getRateLimit|getRateLimit\s*=" --type ts --type tsx

Repository: AOSSIE-Org/OrgExplorer

Length of output: 93


🏁 Script executed:

# Check more of the fetchWithRetry function
sed -n '130,170p' src/lib/github/client.ts

Repository: AOSSIE-Org/OrgExplorer

Length of output: 1524


🏁 Script executed:

# Search for usage of x-ratelimit headers
rg -n "x-ratelimit|ratelimit" src/lib/github/client.ts

Repository: AOSSIE-Org/OrgExplorer

Length of output: 48


🏁 Script executed:

# Check the full getRateLimit implementation
sed -n '71,95p' src/lib/github/client.ts

Repository: AOSSIE-Org/OrgExplorer

Length of output: 695


Extract rate limit info from response headers instead of preflight checks.

Calling getRateLimit() before every request attempt adds a separate API call, doubling traffic and consuming unnecessary quota. GitHub API responses include x-ratelimit-remaining and x-ratelimit-reset headers that should be extracted from the actual request and used for backoff decisions, eliminating the preflight check entirely.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/github/client.ts` around lines 133 - 140, The current loop calls
getRateLimit() before each request which doubles API calls; instead remove the
preflight getRateLimit() invocation in the retry loop and extract rate-limit
info from the actual request response headers (e.g., read
response.headers.get('x-ratelimit-remaining') and 'x-ratelimit-reset') to decide
backoff and delay; update the retry logic inside the for-loop that uses
maxRetries/attempt and delay() so it inspects those headers after a response (or
on error when available) and computes waitTime = (reset - Date.now()/1000)*1000,
logging and awaiting delay(waitTime) when remaining < 1 and waitTime > 0.

const perPage = 100;

while (allIssues.length < maxResults) {
const query = `org:${org} is:issue is:open${sinceDate ? ` created:>${sinceDate}` : ''}`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

searchAllIssues currently fetches only open issues.

Line 285 hardcodes is:open, so closed issues are excluded. That undercounts contributor activity for an “all issues” API.

💡 Proposed fix
-    const query = `org:${org} is:issue is:open${sinceDate ? ` created:>${sinceDate}` : ''}`;
+    const query = `org:${org} is:issue${sinceDate ? ` created:>${sinceDate}` : ''}`;
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const query = `org:${org} is:issue is:open${sinceDate ? ` created:>${sinceDate}` : ''}`;
const query = `org:${org} is:issue${sinceDate ? ` created:>${sinceDate}` : ''}`;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/github/client.ts` at line 285, The searchAllIssues implementation
currently forces only open issues by building the query string with "is:open";
update the query construction in searchAllIssues to remove or make the "is:open"
filter conditional so it can include closed issues when callers request "all"
(or default to no state filter for an all-issues endpoint). Locate the query
string assembly where `const query = \`org:${org} is:issue is:open${sinceDate ?
\` created:>${sinceDate}\` : ''}\`;` is defined and adjust it to either omit
`is:open` or accept a parameter (e.g., issueState) to append `is:open` or
`is:closed` only when needed, ensuring callers can request all issues. Ensure
tests or call sites of searchAllIssues are updated to pass the desired state
when appropriate.

Comment on lines +34 to +42
export interface ContributorSummary {
login: string;
avatarUrl: string;
pullRequestCount: number;
issueCount: number;
mergedPRCount: number;
repositories: Map<string, number>;
totalContributions: number;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check how ContributorSummary is constructed and if Map serialization is handled
rg -n "ContributorSummary|repositories.*Map|new Map" --type ts -A 3 -B 1

Repository: AOSSIE-Org/OrgExplorer

Length of output: 4007


🏁 Script executed:

#!/bin/bash
# Find where fetchOrgActivity is used and how the result is handled
rg -n "fetchOrgActivity|ContributorMatrixData" --type ts --type tsx -A 5 -B 2 | head -80

Repository: AOSSIE-Org/OrgExplorer

Length of output: 93


🏁 Script executed:

#!/bin/bash
# Find where fetchOrgActivity is used and how the result is handled
rg -n "fetchOrgActivity|ContributorMatrixData" --type ts -A 5 -B 2 | head -100

Repository: AOSSIE-Org/OrgExplorer

Length of output: 6954


🏁 Script executed:

#!/bin/bash
# Get the full ExportContributorDataUseCase implementation
sed -n '80,95p' src/features/contributor-activity/useCases.ts

Repository: AOSSIE-Org/OrgExplorer

Length of output: 677


Fix JSON serialization of Map fields in export function.

The ExportContributorDataUseCase.execute method calls JSON.stringify(data) directly on ContributorMatrixData, which contains Map<string, number> fields (repositories and activityByContributor). Maps serialize as empty objects {}, causing complete data loss in JSON exports. The CSV export correctly converts Maps using Array.from(entries()), but the JSON export does not. Convert Maps to plain objects before serialization or implement a custom replacer function for JSON.stringify.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/github/types.ts` around lines 34 - 42, The JSON export currently
serializes ContributorMatrixData containing Map fields (repositories and
activityByContributor) which become empty objects; in
ExportContributorDataUseCase.execute convert any Map<string, number> fields
(e.g., ContributorSummary.repositories and top-level activityByContributor in
ContributorMatrixData) into plain objects before calling JSON.stringify — for
example, replace Maps with Object.fromEntries(map) or build a plain object via
Array.from(map.entries()) for each map, or supply a JSON.stringify replacer that
transforms Map instances to plain objects; ensure you perform this
transformation for ContributorSummary objects inside the matrix as well as the
top-level maps so the exported JSON contains the actual key/value pairs.

Comment on lines +56 to +60
export interface RateLimitInfo {
limit: number;
remaining: number;
reset: number;
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion | 🟠 Major

Duplicate RateLimitInfo interface definition.

This interface is already defined identically in src/lib/github/client.ts (lines 2-6). Consider removing one and importing from a single source to avoid maintenance burden and potential drift.

♻️ Suggested approach

Either:

  1. Export from types.ts and import in client.ts, or
  2. Remove from types.ts and import from client.ts
-export interface RateLimitInfo {
-  limit: number;
-  remaining: number;
-  reset: number;
-}
+// Re-export from client.ts or consolidate all types here
+export type { RateLimitInfo } from './client';
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/lib/github/types.ts` around lines 56 - 60, The RateLimitInfo interface is
duplicated; consolidate to a single source by keeping one declaration and
importing it where needed: either export RateLimitInfo from the types module and
remove the duplicate in the client module (then import RateLimitInfo in
client.ts), or delete RateLimitInfo from types.ts and import the single
definition from client.ts; update any imports/usages to reference the retained
symbol RateLimitInfo and remove the redundant interface declaration.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[FEATURE]: "GSoC Mentor Dashboard" & Cross-Repository Contributor Activity Matrix

1 participant